1. 组件的本质:虚拟 DOM 节点
在 Vue.js 中,组件本质上是一种特殊的虚拟 DOM 节点。虚拟 DOM(VNode)通过 type
属性来区分不同的节点类型。例如:
- 普通标签:
vnode.type
为字符串,如 'div'
。
- 文本节点:
vnode.type
为 Text
。
- 片段(Fragment):
vnode.type
为 Fragment
。
- 组件:
vnode.type
为组件的选项对象。
例如,一个组件的虚拟 DOM 表示如下:
const MyComponent = {
name: 'MyComponent',
data() {
return { foo: 1 }
},
render() {
return { type: 'div', children: `foo 的值是: ${this.foo}` }
}
}
const vnode = {
type: MyComponent // 组件选项对象
}
渲染器通过检查 vnode.type
的类型来决定如何处理节点。当 type
是一个对象时,渲染器识别其为组件,并调用 mountComponent
或 patchComponent
函数来完成挂载或更新。
渲染器的核心逻辑
渲染器的 patch
函数是处理虚拟 DOM 的核心入口,它根据 vnode.type
的类型分发处理逻辑:
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
// 处理普通元素
} else if (type === Text) {
// 处理文本节点
} else if (type === Fragment) {
// 处理片段
} else if (typeof type === 'object') {
// 处理组件
if (!n1) {
mountComponent(n2, container, anchor)
} else {
patchComponent(n1, n2, anchor)
}
}
}
对于组件类型,mountComponent
负责初次挂载,patchComponent
负责更新。这种设计让渲染器能够统一处理不同类型的节点。
2. 组件的挂载与渲染
组件的挂载由 mountComponent
函数完成,其核心步骤是:
- 获取组件的选项对象(如
render
函数和 data
函数)。
- 执行
render
函数生成虚拟 DOM(子树,subTree)。
- 调用
patch
函数将子树渲染为真实 DOM。
以下是 mountComponent
的简化实现:
function mountComponent(vnode, container, anchor) {
const componentOptions = vnode.type
const { render } = componentOptions
const subTree = render()
patch(null, subTree, container, anchor)
}
这个过程展示了组件如何通过 render
函数描述页面内容,并最终通过渲染器转换为真实 DOM。
3. 组件状态与响应式自更新
组件状态的初始化
组件可以通过 data
函数定义自身状态,这些状态需要是响应式的,以便在状态变化时触发更新。例如:
const MyComponent = {
name: 'MyComponent',
data() {
return { foo: 'hello world' }
},
render() {
return { type: 'div', children: `foo 的值是: ${this.foo}` }
}
}
在 mountComponent
中,data
函数的返回值会被包装为响应式对象:
function mountComponent(vnode, container, anchor) {
const { render, data } = vnode.type
const state = reactive(data())
const subTree = render.call(state, state)
patch(null, subTree, container, anchor)
}
通过 reactive
函数,state
成为响应式对象,render
函数通过 this
访问状态数据。
组件的自更新
为了实现状态变化时的自动更新,Vue.js 将组件的渲染任务包装在一个 effect
中:
function mountComponent(vnode, container, anchor) {
const { render, data } = vnode.type
const state = reactive(data())
effect(() => {
const subTree = render.call(state, state)
patch(null, subTree, container, anchor)
})
}
当 state
发生变化时,effect
会触发 render
函数重新执行,从而更新 DOM。然而,频繁的状态变化可能导致多次渲染,造成性能浪费。为此,Vue.js 使用调度器(scheduler)来优化更新。
调度器的实现
调度器通过微任务队列缓冲渲染任务,避免重复执行:
const queue = new Set()
let isFlushing = false
const p = Promise.resolve()
function queueJob(job) {
queue.add(job)
if (!isFlushing) {
isFlushing = true
p.then(() => {
try {
queue.forEach(job => job())
} finally {
isFlushing = false
queue.clear = 0
}
})
}
}
function mountComponent(vnode, container, anchor) {
const { render, data } = vnode.type
const state = reactive(data())
effect(() => {
const subTree = render.call(state, state)
patch(null, subTree, container, anchor)
}, { scheduler: queueJob })
}
通过 queueJob
,渲染任务被放入微任务队列,多次状态变化只会触发一次渲染,显著提升性能。
4. 组件实例与生命周期
组件实例
组件实例是一个对象,维护组件运行时的状态,例如:
state
:组件的响应式数据。
isMounted
:标记组件是否已挂载。
subTree
:组件渲染的子树。
通过组件实例,渲染器可以区分挂载和更新操作:
function mountComponent(vnode, container, anchor) {
const { render, data } = vnode.type
const state = reactive(data())
const instance = {
state,
isMounted: false,
subTree: null
}
vnode.component = instance
effect(() => {
const subTree = render.call(state, state)
if (!instance.isMounted) {
patch(null, subTree, container, anchor)
instance.isMounted = true
} else {
patch(instance.subTree, subTree, container, anchor)
}
instance.subTree = subTree
}, { scheduler: queueJob })
}
生命周期钩子
Vue.js 的生命周期钩子(如 beforeCreate
、created
等)在适当的时机被调用:
function mountComponent(vnode, container, anchor) {
const { render, data, beforeCreate, created, beforeMount, mounted } = vnode.type
beforeCreate && beforeCreate()
const state = reactive(data())
const instance = { state, isMounted: false, subTree: null }
vnode.component = instance
created && created.call(state)
effect(() => {
const subTree = render.call(state, state)
if (!instance.isMounted) {
beforeMount && beforeMount.call(state)
patch(null, subTree, container, anchor)
instance.isMounted = true
mounted && mounted.call(state)
} else {
// 更新逻辑
}
instance.subTree = subTree
}, { scheduler: queueJob })
}
生命周期钩子通过数组存储,支持多个钩子函数的注册。
5. Props 与被动更新
Props 的解析
组件的 props
定义了组件接受的属性,渲染器通过 resolveProps
函数解析 vnode.props
和组件选项中的 props
:
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
for (const key in propsData) {
if (key in options || key.startsWith('on')) {
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
return [props, attrs]
}
props
是组件显式声明的属性,attrs
是未声明的属性。事件监听器(如 onChange
)也被视为 props
。
被动更新
当父组件的 props
变化时,子组件需要更新。patchComponent
函数处理这种被动更新:
function patchComponent(n1, n2, anchor) {
const instance = (n2.component = n1.component)
const { props } = instance
if (hasPropsChanged(n1.props, n2.props)) {
const [nextProps] = resolveProps(n2.type.props, n2.props)
for (const k in nextProps) {
props[k] = nextProps[k]
}
for (const k in props) {
if (!(k in nextProps)) delete props[k]
}
}
}
function hasPropsChanged(prevProps, nextProps) {
const nextKeys = Object.keys(nextProps)
if (nextKeys.length !== Object.keys(prevProps).length) {
return true
}
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i]
if (nextProps[key] !== prevProps[key]) return true
}
return false
}
通过 shallowReactive
,props
的变化会触发组件重新渲染。
6. Setup 函数与组合式 API
Vue.js 3 引入了 setup
函数,用于组合式 API。它在组件挂载时执行一次,返回值可以是:
- 渲染函数:直接作为组件的
render
函数。
- 对象:暴露的数据供模板使用。
示例:
const Comp = {
setup() {
const count = ref(0)
return { count }
},
render() {
return { type: 'div', children: `count is: ${this.count}` }
}
}
setup
函数接收 props
和 setupContext
(包含 attrs
、emit
、slots
等):
function mountComponent(vnode, container, anchor) {
const { render, data, setup } = vnode.type
const state = data ? reactive(data()) : null
const [props, attrs] = resolveProps(vnode.type.props, vnode.props)
const instance = { state, props: shallowReactive(props), isMounted: false, subTree: null }
const setupContext = { attrs }
const setupResult = setup(shallowReadonly(instance.props), setupContext)
let setupState = null
if (typeof setupResult === 'function') {
if (render) console.error('setup 返回渲染函数,render 选项将被忽略')
render = setupResult
} else {
setupState = setupResult
}
const renderContext = new Proxy(instance, {
get(t, k) {
if (k in t.state) return t.state[k]
if (k in t.props) return t.props[k]
if (setupState && k in setupState) return setupState[k]
console.error('属性不存在')
}
})
}
7. 自定义事件与 emit
emit
函数用于触发自定义事件。事件名称(如 change
)会被转换为 onChange
并存储在 props
中:
function emit(event, ...payload) {
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
const handler = instance.props[eventName]
if (handler) {
handler(...payload)
} else {
console.error('事件不存在')
}
}
emit
被添加到 setupContext
,供 setup
函数使用。
8. 插槽的实现
插槽允许父组件为子组件提供自定义内容。子组件的模板使用 <slot>
标签,父组件通过 v-slot
指令填充内容。插槽内容被编译为函数,存储在 vnode.children
中:
function mountComponent(vnode, container, anchor) {
const slots = vnode.children || {}
const instance = { state, props: shallowReactive(props), isMounted: false, subTree: null, slots }
const setupContext = { attrs, emit, slots }
const renderContext = new Proxy(instance, {
get(t, k) {
if (k === '$slots') return t.slots
// 其他逻辑
}
})
}
插槽函数的调用结果作为子组件的渲染内容。
9. 生命周期钩子的注册
Vue.js 3 的组合式 API 提供 onMounted
等函数注册生命周期钩子。通过 currentInstance
维护当前组件实例:
let currentInstance = null
function setCurrentInstance(instance) {
currentInstance = instance
}
function onMounted(fn) {
if (currentInstance) {
currentInstance.mounted.push(fn)
} else {
console.error('onMounted 只能在 setup 中调用')
}
}
function mountComponent(vnode, container, anchor) {
const instance = { state, props: shallowReactive(props), isMounted: false, subTree: null, mounted: [] }
setCurrentInstance(instance)
const setupResult = setup(shallowReadonly(instance.props), setupContext)
setCurrentInstance(null)
effect(() => {
const subTree = render.call(renderContext, renderContext)
if (!instance.isMounted) {
patch(null, subTree, container, anchor)
instance.isMounted = true
instance.mounted.forEach(hook => hook.call(renderContext))
}
}, { scheduler: queueJob })
}